﻿/* Autor: Achim Schnell */

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using AppFx.Reflection.Attributes;
using AppFx.Reflection.Types;
using System.Collections;

namespace AppFx.ComponentModel.Descriptive
{
	/// <summary>
	/// Diese Klasse stellt PropertyDescriptors zu den ihr übergebenen
	/// Typen zur Verfügung, welche kompilierte Accessoren nutzen - und
	/// damit für eine schnellere Zugriffsmöglichkeit auf unbekannte Eigenschaften
	/// ermöglichen.
	/// </summary>
	/// <remarks>
	/// Bitte beachten:
	/// Jeder Aufruf einer Methode nutzt hier Reflection >> Overhead!
	/// Die Rückgaben sollten (soweit möglich / sinnvoll) gecached werden.
	/// 
	/// Diese Klasse untersucht zudem untergeordnete Eigenschaften. (Nested Properties)
	/// </remarks>
	public class DeepCompiledTypeDescriptionProvider : CompiledTypeDescriptionProvider
	{
		#region Properties

		/// <summary>
		/// Gibt oder setzt ob komplexe Typen ausgeschlossen werden sollen.
		/// </summary>
		/// <remarks>
		/// Die Verwendung des IncludeAttributs überschreibt das hiermit
		/// festgelegt Standardverhalten.
		/// </remarks>
		public bool ExcludeComplexTypes { get; set; }

		#endregion

		#region Constructors

		public DeepCompiledTypeDescriptionProvider()
		{
			ExcludeComplexTypes = true;
		}

		#endregion

		#region Methods

		/// <summary>
		/// Erstellt eine Liste mit PropertyDescriptors zum übergebenen Typ.
		/// </summary>
		/// <param name="type">Typ, für den die PropertyDescriptors erstellt werden sollen.</param>
		/// <returns>Eine Liste aller erstellten PropertyDescriptors</returns>
		/// <exception cref="T:System.ArgumentNullException">Wird geworfen, wenn der Parameter <paramref name="type"/> NULL ist.</exception>
		public override IList<PropertyDescriptor> GetDescriptors(Type type)
		{
			if (type == null)
				throw new ArgumentNullException("type");

			IList<PropertyDescriptor> descriptors = GetBaseDescriptors(type);
			descriptors = RemoveComplexTypes(descriptors);

			foreach (var exclusion in Exclusions)
			{
				IList<PropertyDescriptor> excludedDescriptors = descriptors.Where(exclusion).ToList();
				descriptors = descriptors.Except(excludedDescriptors).ToList();
			}

			return descriptors;
		}

		/// <summary>
		/// Erstellt alle PropertyDescriptors zu Haupt- und Untertypen.
		/// (Unter Berücksichtigung des Attributs 'ExpandableAttribute'.
		/// </summary>
		/// <param name="type">Typ der Komponente, für welche die PropertyDescriptors erstellt werden sollen.</param>
		/// <returns>Ein Wörterbuch (Key:Name der Eigenschaft, Value: PropetyDescriptor)</returns>
		protected virtual IList<PropertyDescriptor> GetBaseDescriptors(Type type)
		{
			PropertyInfo[] pis = type.GetProperties();
			List<PropertyDescriptor> descriptors = new List<PropertyDescriptor>();

			if (pis == null || pis.Length == 0)
				return descriptors;

			IList<PropertyDescriptor> childDescriptors = new List<PropertyDescriptor>();
			
			for (int i = 0; i < pis.Length; i++)
			{
				CompiledPropertyDescriptor descriptor = new CompiledPropertyDescriptor(type, pis[i]);
				descriptors.Add(descriptor);

				if (!TypeHelper.IsSimple(descriptor.PropertyType) && !TypeHelper.IsIEnumerable(descriptor.PropertyType))
				{
					PropertyInfo pi = ((PropertyDescriptorEx)descriptor).PropertyInfo;
					ExpandableAttribute[] expandables = null;
					bool expandable = (AttributeHelper.IsAttributeUsed<ExpandableAttribute>(pi, ref expandables)
						? AttributeHelper.GetAttributeValue<bool>(expandables[0], "Expandable")
						: true);
					if (expandable)
						foreach (var childDescriptor in GetChildDescriptors(type, descriptor))
							childDescriptors.Add(childDescriptor);
				}
			}

			foreach (var childDescriptor in childDescriptors)
				descriptors.Add(childDescriptor);
			return descriptors;
		}

		/// <summary>
		/// Entfernt alle komplexe Typen unter Berücksichtigung der Eigenschaft 'ExcludeComplexTypes',
		/// sowie des Attributs 'IncludeAttribute'.
		/// </summary>
		/// <param name="descriptors"></param>
		protected virtual IList<PropertyDescriptor> RemoveComplexTypes(IList<PropertyDescriptor> descriptors)
		{
			IList<PropertyDescriptor> complexToExclude = new List<PropertyDescriptor>();
			foreach (var descriptor in descriptors)
			{
				if (!TypeHelper.IsSimple(descriptor.PropertyType))
				{
					PropertyInfo pi = ((PropertyDescriptorEx)descriptor).PropertyInfo;
					IncludeAttribute[] includes = null;
					if ((AttributeHelper.IsAttributeUsed<IncludeAttribute>(pi, ref includes))
						? !AttributeHelper.GetAttributeValue<bool>(includes[0], "Include")
						: ExcludeComplexTypes)
						complexToExclude.Add(descriptor);
				}
			}
			return descriptors.Except(complexToExclude).ToList();
		}

		/// <summary>
		/// Erstellt ein Wörterbuch mit NestedPropertyDescriptors zum übergebenen Typ.
		/// Key:   Name der Eigenschaft, ein "." deutet NestedProperties hin.
		/// Value: PropertyDescriptor zur Eigenschaft.
		/// </summary>
		/// <param name="componentType">Typ, für den die PropertyDescriptors erstellt werden sollen.</param>
		/// <param name="parent">Der übergeordnete PropertyDescriptor.</param>
		/// <returns>
		/// Ein Wörterbuch (Key:Name der Eigenschaft, Value: PropetyDescriptor)
		/// </returns>
		protected virtual IList<PropertyDescriptor> GetChildDescriptors(Type componentType, PropertyDescriptor parent)
		{
			IList<PropertyDescriptor> descriptors = new List<PropertyDescriptor>();

			foreach (PropertyInfo pi in parent.PropertyType.GetProperties())
			{
				CompiledNestedPropertyDescriptor descriptor = new CompiledNestedPropertyDescriptor(componentType, pi, parent);

				descriptors.Add(descriptor);
				if (!TypeHelper.IsSimple(descriptor.PropertyType))
				{
					foreach (var childDescriptor in GetChildDescriptors(componentType, descriptor))
					{
						descriptors.Add(childDescriptor);
					}
				}
			}

			return descriptors;
		}

		#endregion
	}
}
